查看原文
其他

安卓 Bitmap 高效加载,那些你必须掌握的稀碎知识点

推荐关注↓

背景

Bitmap 是 Android 系统中的图像处理中最重要类之一。对于大多数 App,如何高效加载 Bitmap 显得至关重要。Bitmap 可以获取图像文件信息,对图像进行剪切、旋转、缩放,压缩等操作,并可以以指定格式保存图像文件。现在已经有很多主流的框架,如 Glide,Fresco,Picasso 等帮我们快速实现。其实这其中都包含了图片高效加载的策略,缓存策略等。这些框架当然很优秀,但有时候需要我们自己实现自己的图片加载框架的时候,我们也应该胸有成竹。

Bitmap 内存占用

Bitmap 的内存占用 = 宽 * 高 * 单位像素所占用字节,只与这三个因素有关,和图片是webp,png,jpg 没有关系。

Bitmap Config

颜色用 ARGB 来表示,A 表示 Alpha,即透明度;R 表示 red,即红色;G 表示 green,即绿色;B 表示 blue,即蓝色。Bitmap 的色彩也是用 ARGB 来表示的

Bitmap.Config中有Bitmap.Config.ALPHA_8、Bitmap.Config.RGB_565、Bitmap.Config.ARGB_4444、 Bitmap.Config.ARGB_8888有四个枚举变量。

Bitmap.Config.ALPHA_8表示:每个像素占8位,没有色彩,只有透明度A-8,共8位。Bitmap.Config.ARGB_4444表示:每个像素占16位,A-4,R-4,G-4,B-4,共4+4+4+4=16位。Bitmap.Config.RGB_565表示:每个像素占16位,没有透明度,R-5,G-6,B-5,共5+6+5=16位。Bitmap.Config.ARGB_8888表示:每个像素占32位,A-8,R-8,G-8,B-8,共8+8+8+8=32位。位数越高,那么可存储的颜色信息越多,图像也就越逼真。

drawable 不同分辨率文件夹放置和 Bitmap 大小的关系

具体的关系可以参考我的这篇文章Android 屏幕适配,那些你必须掌握的稀碎知识点。

减少内存占用

  1. 压缩,采样率 — inSampleSize,大于等于 1,通过对它的设置,对图片的像素的高和宽进行缩放,官方文档指出,inSampleSize 的取值应该总是 2 的指数,如 1,2,4,8 等。如果外界传入的 inSampleSize 的值不为 2 的指数,那么系统会向下取整并选择一个最接近 2 的指数来代替。同时图片不宜直接加载到内存中。

    通常我们优化Bitmap时,当需要做性能优化或者防止 OOM(Out Of Memory),我们通常会使用Bitmap.Config.RGB_565 这个配置,因为Bitmap.Config.ALPHA_8 只有透明度,显示一般图片没有意义,Bitmap.Config.ARGB_4444 显示图片不清楚,Bitmap.Config.ARGB_8888 占用内存最多。

    CompressFormat解析

    Bitmap.CompressFormat.JPEG:表示以 JPEG 压缩算法进行图像压缩,压缩后的格式可以是".jpg"或者".jpeg",是一种有损压缩。

    Bitmap.CompressFormat.PNG:表示以 PNG 压缩算法进行图像压缩,压缩后的格式可以是".png",是一种无损压缩。

    Bitmap.CompressFormat.WEBP:表示以 WebP 压缩算法进行图像压缩,压缩后的格式可以是".webp",是一种有损压缩,质量相同的情况下,WebP 格式图像的体积要比 JPEG 格式图像小40%。美中不足的是,WebP 格式图像的编码时间“比 JPEG 格式图像长8倍”。

  2. 复用已经开辟出的内存,一般我们都会使用 LruCache(最近最少使用) 来缓存 Bitmap,当某个 Bitmap 被移除后,会调用 entryRemoved 方法进行移除,这个时候我们可以把这个移除的 Bitmap 放到复用池中进行复用(因为已经开辟出内存了),需要注意的是,我的复用池并没有设置大小,是因为每次 getReusable() 后都会将其移除,而且也不必担心复用池太大的问题(bitmap 很容易被复用,安卓系统的优化)。

    //复用方法    
    public static Bitmap resizeBitmap(Context context, int id, int maxW, int maxH, boolean hasAlpha, Bitmap reusable) {

        Resources resources = context.getResources();

        BitmapFactory.Options options = new BitmapFactory.Options();
        // 设置为true后,再去解析,就只解析 out 参数
        options.inJustDecodeBounds = true;

        BitmapFactory.decodeResource(resources, id, options);
        int w = options.outWidth;
        int h = options.outHeight;
        options.inSampleSize = calcuteInSampleSize(w, h, maxW, maxH);

        if (!hasAlpha) {
            options.inPreferredConfig = Bitmap.Config.RGB_565;
        }
        options.inJustDecodeBounds = false;

        // 复用, inMutable 为true 表示易变
        options.inMutable = true;
        options.inBitmap = reusable;

        return BitmapFactory.decodeResource(resources, id, options);
    }


    /**
     * 3.0 之前不能复用
     * 3.0-4.4 宽高一样,inSampleSize = 1
     * 4.4 只要小于等于就行了
     *
     * @param w
     * @param h
     * @param inSampleSize
     * @return
     */

    public Bitmap getReusable(int w, int h, int inSampleSize) {
        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.HONEYCOMB) {
            return null;
        }
        Bitmap reusable = null;

        Iterator<WeakReference<Bitmap>> iterator = reusablePool.iterator();
        while (iterator.hasNext()) {
            Bitmap bitmap = iterator.next().get();
            if (bitmap != null) {
                if (checkInBitmap(bitmap, w, h, inSampleSize)) {
                    reusable = bitmap;
                    iterator.remove();
                    break;
                }
            } else {
                iterator.remove();
            }
        }

        return reusable;

    }

    /**
     * 校验bitmap 是否满足条件
     *
     * @param bitmap
     * @param w
     * @param h
     * @param inSampleSize
     * @return
     */

    private boolean checkInBitmap(Bitmap bitmap, int w, int h, int inSampleSize) {
        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT) {
            return bitmap.getWidth() == w && bitmap.getHeight() == h && inSampleSize == 1;
        }

        if (inSampleSize > 1) {
            w /= inSampleSize;
            h /= inSampleSize;
        }
        int byteCount = w * h * getBytesPerPixel(bitmap.getConfig());
        // 图片内存 系统分配内存
        return byteCount <= bitmap.getAllocationByteCount();
    }

内存加载策略

  1. 先使用内存缓存 LruCache

  2. 使用复用缓存(内存缓存中 LruCache 中移除出来的,加入到复用缓存中)

  3. 使用磁盘缓存(DiskLruCache)

  4.  如果都找不到,再从网络获取(然后放入内存,放入磁盘)

private LruCache<String, Bitmap> lruCache;
private Set<WeakReference<Bitmap>> reusablePool;
private DiskLruCache diskLruCache;
public void init(Context context, String dir) {

        // 复用池
        reusablePool = Collections.synchronizedSet(new HashSet<WeakReference<Bitmap>>());

        // 内存大小
        ActivityManager am = (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE);
        int memoryClass = am.getMemoryClass();

        //memoryClass * 1024 * 1024 / 8
        lruCache = new LruCache<String, Bitmap>(memoryClass * 1024 * 1024 / 8) {
            // 返回的一张图片大小
            @Override
            protected int sizeOf(String key, Bitmap value) {
                if (Build.VERSION.SDK_INT > Build.VERSION_CODES.KITKAT) {
                    return value.getAllocationByteCount();
                }
                return value.getByteCount();
            }

            @Override
            protected void entryRemoved(boolean evicted, String key, Bitmap oldValue, Bitmap newValue) {
                if (oldValue.isMutable()) {
                    // 3.0 bitmap 缓存 native
                    // <8.0  bitmap 缓存 java
                    // 8.0 native
                   // 将bitmap 和一个队列进行关联(弱引用),当被回收的时候队列中会有该bitmap的引用,然后进行获取,回收bitmap
                    reusablePool.add(new WeakReference<Bitmap>(oldValue, getReferenceQueue()));
                } else {

                    oldValue.recycle();  //不可复用,就进行回收
                }
            }
        };
        try {
            diskLruCache = DiskLruCache.open(new File(dir), BuildConfig.VERSION_CODE, 110 * 1024 * 1024);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }


    private ReferenceQueue<Bitmap> getReferenceQueue() {
        if (referenceQueue == null) {  //如果为空,就进行创建,创建一个线程,一直在轮询查找是否有被回收的bitmap 需要回收,前面调用reusablePool.add(new WeakReference<Bitmap>(oldValue, getReferenceQueue())); 将bitmap 和一个队列进行关联(弱引用),当被回收的时候队列中会有该bitmap的引用,然后进行获取,回收bitmap
            referenceQueue = new ReferenceQueue<>();
            new Thread(new Runnable() {
                @Override
                public void run() {
                    while (!shutDown) {
                        try {
                            Reference<? extends Bitmap> remove = referenceQueue.remove();//阻塞方法
                            Bitmap bitmap = remove.get();
                            if (bitmap != null && !bitmap.isRecycled()) {
                                bitmap.recycle();
                            }

                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                }
            }).start();
        }
        return referenceQueue;
    }

虽然在 android 8.0 之后,NativeAllocationRegistry可以辅助回收Java对象所申请的native 内存,但是通过上面轮询阻塞的方式,能够更快的进行内存回收。

内存回收

  1. 2.3~4.4 可以通过 inInputShareable、inPurgeable 让 Bitmap 的内存在 native 层分配(已废弃)
  2. 8.0 之前的 Bitmap 像素数据基本存储在 Java heap
  3. 8.0 之后的 Bitmap 像素数据基本存储在 native heap,NativeAllocationRegistry 是 Android 8.0 引入的一种辅助自动回收 native 内存的一种机制,当 Java 对象因为 GC 被回收后,NativeAllocationRegistry 可以辅助回收 Java 对象所申请的 native 内存
  4. 2.3 之前的像素存储需要的内存是在 native 上分配的,并且生命周期不太可控,可能需要用户自己回收。2.3-7.1 之间,Bitmap 的像素存储在 Dalvik 的 Java 堆上,当然,4.4 之前的甚至能在匿名共享内存上分配(Fresco 采用),而8.0 之后的像素内存又重新回到 native 上去分配,不需要用户主动回收,8.0 之后图像资源的管理更加优秀,极大降低了 OOM。

大图加载

BitmapRegionDecoder 大图加载利器

看看是横向还是纵向滑动,每次加载固定一小块区域(Rect),然后结合手势,判断是什么方向的移动,然后综合进行处理。

总结

Bitmap 其实是个很琐碎的知识点,涉及很多方面的知识,掌握好这些,后面我们在学习优秀的第三方框架也会更容易了,喜欢的点个赞吧~


转自:掘金  伤心的猪大肠

https://juejin.cn/post/6945227597274054664

- EOF -

推荐阅读  点击标题可跳转

1、线程切换哪家强?RxJava与Flow的操作符对比

2、Retrofit 枯燥无趣的源码分析

3、关于Android架构,你是否还在生搬硬套?


看完本文有收获?请分享给更多人

 推荐关注「安卓开发精选」,提升安卓开发技术

点赞和在看就是最大的支持❤️

: . Video Mini Program Like ,轻点两下取消赞 Wow ,轻点两下取消在看

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存